/*
 
 Converts A class to a dictionary, used for serializing dictionaries to JSON
 
 Supported objects:
 - Serializable derived classes (sub classes)
 - Arrays of Serializable
 - NSData
 - String, Numeric, and all other NSJSONSerialization supported objects
 
 */

import Foundation

open class Serializable: NSObject {
    
    /**
     Converts the class to a dictionary.
     
     :returns: The class as an NSDictionary.
     */
    open func toDictionary() -> NSDictionary {
        let propertiesDictionary = NSMutableDictionary()
        let mirror = Mirror(reflecting : self)
        
        for (child, childValue) in mirror.children {
            //            if child == "totalAmount" {
            //                let hit = 0
            //            }
            if let propValue = self.unwrap(childValue) {
                let propName = child!
                if let serializablePropValue = propValue as? Serializable {
                    propertiesDictionary.setValue(serializablePropValue.toDictionary(), forKey: propName)
                } else if let arrayPropValue = propValue as? [Serializable] {
                    var subArray = [NSDictionary]()
                    for item in arrayPropValue {
                        subArray.append(item.toDictionary())
                    }
                    
                    propertiesDictionary.setValue(subArray, forKey: propName)
                } else if propValue is Int || propValue is Double || propValue is Float {
                    propertiesDictionary.setValue(propValue, forKey: propName)
                } else if let dataPropValue = propValue as? Data {
                    propertiesDictionary.setValue(dataPropValue.base64EncodedString(options: .lineLength64Characters), forKey: propName)
                } else if let datePropValue = propValue as? Date {
                    propertiesDictionary.setValue(datePropValue.toShortDateString(), forKey: propName)
                    //propertiesDictionary.setValue(datePropValue.timeIntervalSince1970, forKey: propName)
                } else if let boolPropValue = propValue as? Bool {
                    propertiesDictionary.setValue(boolPropValue, forKey: propName)
                } else if let rect = propValue as? CGRect {
                    #if DEBUG
                       print("the property \(rect) can not be serializable")
                    #endif
                } else if let image = propValue as? UIImage {
                    #if DEBUG
                        print("the property \(image) can not be serializable")
                    #endif
                } else {
                    propertiesDictionary.setValue(propValue, forKey: propName)
                }
            }
        }
        
        return propertiesDictionary
        
    }
    
    /**
     Converts the class to JSON.
     
     :returns: The class as JSON, wrapped in NSData.
     */
    open func toJson(_ prettyPrinted : Bool = false) -> Data? {
        let dictionary = self.toDictionary()
        
        if JSONSerialization.isValidJSONObject(dictionary) {
            do {
                let json = try JSONSerialization.data(withJSONObject: dictionary, options: (prettyPrinted ? .prettyPrinted : JSONSerialization.WritingOptions()))
                return json
            } catch {
                //currently swift will not catch NSInvalidArgumentException exception
                print("ERROR: Unable to serialize json, error: \(error)")
                NotificationCenter.default.post(name: Notification.Name(rawValue: "CrashlyticsLogNotification"), object: self, userInfo: ["string": "unable to serialize json, error: \(error)"])
            }
        }
        
        return nil
    }
    
    /**
     Converts the class to a JSON string.
     
     :returns: The class as a JSON string.
     */
    open func toJsonString(_ prettyPrinted : Bool = false) -> String? {
        if let jsonData = self.toJson(prettyPrinted) {
            return NSString(data: jsonData, encoding: String.Encoding.utf8.rawValue) as String?
        }
        
        return nil
    }
}

extension Serializable {
    
    /**
     Unwraps 'any' object. See http://stackoverflow.com/questions/27989094/how-to-unwrap-an-optional-value-from-any-type
     
     :returns: The unwrapped object.
     */
    fileprivate func unwrap(_ any: Any) -> Any? {
        let mi = Mirror(reflecting : any)
        
        
        if mi.displayStyle != .optional {
            if let serial = any as? EnumSerializable {
                return serial.serializeValue
            }
            return any
        }
        
        if mi.children.count == 0 {
            return nil
        }
        
        if let (_, some) = mi.children.first {
            let innerMi = Mirror(reflecting: some)
            if innerMi.displayStyle == .enum {
                if let serial = some as? EnumSerializable {
                    return serial.serializeValue
                }
            }
            return some
        }
        return nil
    }
}

public protocol ISerializeCopyableProtocol {
    /**
     for partial properties value update.
     
     - parameter json: partial data json
     */
    func copyValuesWithJson(_ json : JSON)
}

extension Serializable {
    
    /**
     Serialize copy.
     
     - returns: return the same reference with to paramemter
     */
    @discardableResult
    public func serializeCopy<T:ISerializeCopyableProtocol>(to target : T) -> T{
        let dic = self.toCopyDictionary()
        target.copyValuesWithJson(JSON(dic))
        return target
    }
    
    @discardableResult
    public func serializeCopy<T: ISerializeCopyableProtocol & IJSONInitializer>() -> T{
        return serializeCopy(to: T.init(json: JSON.null))
    }
    
    fileprivate func toCopyDictionary() -> NSDictionary {
        let propertiesDictionary = NSMutableDictionary()
        let mirror = Mirror(reflecting : self)
        
        for (child, childValue) in mirror.children {
            if let propValue = self.unwrapCopy(childValue) as AnyObject? {
                let propName = child!
                if let serializablePropValue = propValue as? Serializable {
                    propertiesDictionary.setValue(serializablePropValue.toCopyDictionary(), forKey: propName)
                } else if let arrayPropValue = propValue as? [Serializable] {
                    var subArray = [NSDictionary]()
                    for item in arrayPropValue {
                        subArray.append(item.toCopyDictionary())
                    }
                    
                    propertiesDictionary.setValue(subArray, forKey: propName)
                } else if propValue is Int || propValue is Double || propValue is Float {
                    propertiesDictionary.setValue(propValue, forKey: propName)
                } else if let dataPropValue = propValue as? Data {
                    propertiesDictionary.setValue(dataPropValue.base64EncodedString(options: .lineLength64Characters), forKey: propName)
                } else if let datePropValue = propValue as? Date {
                    propertiesDictionary.setValue(datePropValue.toShortDateString(), forKey: propName)
                    //propertiesDictionary.setValue(datePropValue.timeIntervalSince1970, forKey: propName)
                } else if let boolPropValue = propValue as? Bool {
                    propertiesDictionary.setValue(boolPropValue, forKey: propName)
                } else {
                    propertiesDictionary.setValue(propValue, forKey: propName)
                }
            }
        }
        
        return propertiesDictionary
    }
    
    fileprivate func unwrapCopy(_ any: Any) -> Any? {
        let mi = Mirror(reflecting : any)
        
        
        if mi.displayStyle != .optional {
            if let serial = any as? EnumSerializable {
                return serial.copyValue
            }
            return any
        }
        
        if mi.children.count == 0 {
            return nil
        }
        
        if let (_, some) = mi.children.first {
            let innerMi = Mirror(reflecting: some)
            if innerMi.displayStyle == .enum {
                if let serial = some as? EnumSerializable {
                    return serial.copyValue
                }
            }
            return some
        }
        return nil
    }
}

/** protocol for enum to resolve enum serialize problem. */
public protocol EnumSerializable {
    var serializeValue : AnyObject { get }
    /// for serializeCopy
    var copyValue : AnyObject { get }
}
